from collections import namedtuple, OrderedDict
from copy import copy
from helium._impl import APIImpl
from helium._impl.util.html import get_easily_readable_snippet
from helium._impl.util.inspect_ import repr_args
from selenium.webdriver.common.keys import Keys

import helium._impl


def start_chrome(url: str = None, headless: bool = False, options=None):
    """
    :param url: 要访问的网站网址
    :param headless: 是否隐藏浏览器，即执行脚本时，浏览器是否可见, 默认否 不隐藏
    :param options: 浏览器配置，要使用options时需引入配置模块


    For more advanced configuration, use the `options` parameter::

        from selenium.webdriver import ChromeOptions
        options = ChromeOptions()
        options.add_argument('--start-maximized')
        options.add_argument('--proxy-server=1.2.3.4:5678')
        start_chrome(options=options)

        如果要关掉的话 执行该函数

        kill_browser()
    """
    return _get_api_impl().start_chrome_impl(url, headless, options)


def start_firefox(url: str = None, headless: bool = False, options=None):
    """
    启动firefox浏览器
    """
    return _get_api_impl().start_firefox_impl(url, headless, options)


def kill_browser():
    """
    关闭浏览器
    """
    _get_api_impl().kill_browser_impl()


def get_url(url):
    """
    访问url
    """

    _get_api_impl().go_to_impl(url)


def set_driver(driver):
    """
    从selenium 驱动引擎 改成helium驱动引擎
    """
    _get_api_impl().set_driver_impl(driver)


def get_driver():
    """
    从helium 驱动引擎改回 selenium驱动引擎
    """
    return _get_api_impl().get_driver_impl()


def refresh():
    """
    刷新页面
    """
    _get_api_impl().refresh_impl()


def input(text, into=None,):
    """
    写入
    :param text: The text to be written.
    :param into: The element to write into.
    :type into: 可以是字符串, 也可以是S() 类

        write("Hello World!")
        write("user12345", into="Username:")
        write("Michael", into=Alert("Please enter your name"))
    """

    _get_api_impl().write_impl(text, into)


def press(key):
    """
    按键操作
    :param key: Key or combination of keys to be pressed.

    普通字母数字直接用’key’就可以了，如果是特殊按键，这用对应的按键名即可，
        press('a')

        press('A')

        press(ENTER)

    组合键位的话用 '+' 连接

        press(CONTROL + 'a')
    """
    _get_api_impl().press_impl(key)


NULL = Keys.NULL
CANCEL = Keys.CANCEL
HELP = Keys.HELP
BACK_SPACE = Keys.BACK_SPACE
TAB = Keys.TAB
CLEAR = Keys.CLEAR
RETURN = Keys.RETURN
ENTER = Keys.ENTER
SHIFT = Keys.SHIFT
LEFT_SHIFT = Keys.LEFT_SHIFT
CONTROL = Keys.CONTROL
LEFT_CONTROL = Keys.LEFT_CONTROL
ALT = Keys.ALT
LEFT_ALT = Keys.LEFT_ALT
PAUSE = Keys.PAUSE
ESCAPE = Keys.ESCAPE
SPACE = Keys.SPACE
PAGE_UP = Keys.PAGE_UP
PAGE_DOWN = Keys.PAGE_DOWN
END = Keys.END
HOME = Keys.HOME
LEFT = Keys.LEFT
ARROW_LEFT = Keys.ARROW_LEFT
UP = Keys.UP
ARROW_UP = Keys.ARROW_UP
RIGHT = Keys.RIGHT
ARROW_RIGHT = Keys.ARROW_RIGHT
DOWN = Keys.DOWN
ARROW_DOWN = Keys.ARROW_DOWN
INSERT = Keys.INSERT
DELETE = Keys.DELETE
SEMICOLON = Keys.SEMICOLON
EQUALS = Keys.EQUALS
NUMPAD0 = Keys.NUMPAD0
NUMPAD1 = Keys.NUMPAD1
NUMPAD2 = Keys.NUMPAD2
NUMPAD3 = Keys.NUMPAD3
NUMPAD4 = Keys.NUMPAD4
NUMPAD5 = Keys.NUMPAD5
NUMPAD6 = Keys.NUMPAD6
NUMPAD7 = Keys.NUMPAD7
NUMPAD8 = Keys.NUMPAD8
NUMPAD9 = Keys.NUMPAD9
MULTIPLY = Keys.MULTIPLY
ADD = Keys.ADD
SEPARATOR = Keys.SEPARATOR
SUBTRACT = Keys.SUBTRACT
DECIMAL = Keys.DECIMAL
DIVIDE = Keys.DIVIDE
F1 = Keys.F1
F2 = Keys.F2
F3 = Keys.F3
F4 = Keys.F4
F5 = Keys.F5
F6 = Keys.F6
F7 = Keys.F7
F8 = Keys.F8
F9 = Keys.F9
F10 = Keys.F10
F11 = Keys.F11
F12 = Keys.F12
META = Keys.META
COMMAND = Keys.COMMAND


def click(element):
    """
    单击
    :param element: The element or point to click.

    示例:
        click("Sign in")
        click(Button("OK"))
        click(Point(200, 300))
        click(ComboBox("File type").top_left + (50, 0))
    """
    if element == "记住我":
        element = CheckBox(element)
        click(element)
    else:
        _get_api_impl().click_impl(element)


def doubleclick(element):
    """
    双击
    :param element: The element or point to click.

    示例:
        doubleclick("Double click here")
        doubleclick(Image("Directories"))
        doubleclick(Point(200, 300))
        doubleclick(TextField("Username").top_left - (0, 20))
    """
    _get_api_impl().doubleclick_impl(element)


def rightclick(element):
    """
    右击
    :param element: The element or point to click.

    示例:
        rightclick("Something")
        rightclick(Point(200, 300))
        rightclick(Image("captcha"))
    """
    _get_api_impl().rightclick_impl(element)


def drag(element, to):
    """
    拖拽, 将一个元素拖拽到指定位置
    :param element: The element or point to drag.
    :param to: The element or point to drag to.


    Drags the given element or point to the given location. For example::

        drag("Drag me!", to="Drop here.")

    """
    _get_api_impl().drag_impl(element, to)


def drag_file(file_path, to):
    """
    拖拽文件
        click("COMPOSE")
        write("example@gmail.com", into="To")
        write("Email subject", into="Subject")
        drag_file(r"C:\\Documents\\notes.txt", to="Drop files here")
    """
    _get_api_impl().drag_file_impl(file_path, to)


def attach_file(file_path, to=None):
    """
    上传文件
    :param file_path: The path of the file to be attached.
    :param to: The file input element to which the file should be attached.

    Allows attaching a file to a file input element. For instance::

        attach_file("c:/test.txt", to="Please select a file:")

    """
    _get_api_impl().attach_file_impl(file_path, to=to)


def press_mouse_on(element):
    """按住左键"""
    _get_api_impl().press_mouse_on_impl(element)


def release_mouse_over(element):
    """松开左键"""
    _get_api_impl().release_mouse_over_impl(element)


def hover(element):
    """
    鼠标悬浮
    :param element: The element or point to hover.
    :type element: str, unicode, :py:class:`HTMLElement`, \
:py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point`

    Hovers the mouse cursor over the given element or point. For example::

        hover("File size")
        hover(Button("OK"))
        hover(Link("Download"))
        hover(Point(200, 300))
        hover(ComboBox("File type").top_left + (50, 0))
    """
    _get_api_impl().hover_impl(element)


def find_all(predicate):
    """
    查找元素组

    "Open"::
        find_all(Button("Open"))

    Other examples are::
        find_all(Window())
        find_all(TextField("Address line 1"))

    write::

        buttons = find_all(Button("Open"))
        leftmost_button = sorted(buttons, key=lambda button: button.x)[0]
    """
    return _get_api_impl().find_all_impl(predicate)


def scroll_down(num_pixels=100):
    """
    向上滑动屏幕
    """
    _get_api_impl().scroll_down_impl(num_pixels)


def scroll_up(num_pixels=100):
    """
    向下滑动屏幕
    """
    _get_api_impl().scroll_up_impl(num_pixels)


def scroll_right(num_pixels=100):
    """
    向右滑动屏幕
    """
    _get_api_impl().scroll_right_impl(num_pixels)


def scroll_left(num_pixels=100):
    """
    向左滑动屏幕
    """
    _get_api_impl().scroll_left_impl(num_pixels)


def select(combo_box, value):
    """
    下拉框选择
    :param combo_box: The combo box whose value should be changed.
    :type combo_box: str, unicode or :py:class:`ComboBox`
    :param value: The visible value of the combo box to be selected.

    Selects a value from a combo box. For example::

        select("Language", "English")
        select(ComboBox("Language"), "English")
    """
    _get_api_impl().select_impl(combo_box, value)


def wait_until(condition_fn, timeout_secs=10, interval_secs=0.5):
    """
    等待元素直到出现
    等待某个条件为真才继续往下执行。默认的超时时间为10s，每0.5查询一次，这俩参数选填。可以设置超时时间和轮询间隔。
    :param condition_fn: A function taking no arguments that represents the \
    condition to be waited for.
    :param timeout_secs: The timeout, in seconds, after which the condition is \
    deemed to have failed.
    :param interval_secs: The interval, in seconds, at which the condition \
    function is polled to determine whether the wait has succeeded.

    Waits until the given condition function evaluates to true. This is most
    commonly used to wait for an element to exist::

        wait_until(Text("Finished!").exists)

    More elaborate conditions are also possible using Python lambda
    expressions. For instance, to wait until a text no longer exists::

        wait_until(lambda: not Text("Uploading...").exists())

    """

    _get_api_impl().wait_until_impl(condition_fn, timeout_secs, interval_secs)


def highlight(element):
    """
    高亮
    :param element: The element to highlight.
    示例:
        highlight("Helium")
        highlight(Button("Sign in"))
    """
    _get_api_impl().highlight_impl(element)


def switch_to(window):
    """
    窗口切换
    """
    _get_api_impl().switch_to_impl(window)


class Config:
    # 隐式等待时间, 默认10s
    implicit_wait_secs = 10


class GUIElement:
    def __init__(self):
        self._driver = _get_api_impl().require_driver()
        self._args = []
        self._kwargs = OrderedDict()
        self._impl_cached = None

    def exists(self):
        """
        用于判断某个元素是否存在，返回true或false
        """
        return self._impl.exists()

    def with_impl(self, impl):
        result = copy(self)
        result._impl = impl
        return result

    @property
    def _impl(self):
        if self._impl_cached is None:
            impl_class = \
                getattr(helium._impl, self.__class__.__name__ + 'Impl')
            self._impl_cached = impl_class(self._driver, *self._args,
                                           **self._kwargs)
        return self._impl_cached

    @_impl.setter
    def _impl(self, value):
        self._impl_cached = value

    def __repr__(self):
        return self._repr_constructor_args(self._args, self._kwargs)

    def _repr_constructor_args(self, args=None, kwargs=None):
        if args is None:
            args = []
        if kwargs is None:
            kwargs = {}
        return '%s(%s)' % (self.__class__.__name__,
                           repr_args(self.__init__, args, kwargs, repr))

    def _is_bound(self):
        return self._impl_cached is not None and self._impl_cached._is_bound()


class HTMLElement(GUIElement):
    def __init__(self,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(HTMLElement, self).__init__()
        self._kwargs['below'] = below
        self._kwargs['to_right_of'] = to_right_of
        self._kwargs['above'] = above
        self._kwargs['to_left_of'] = to_left_of

    @property
    def width(self):
        """元素宽度"""
        return self._impl.width

    @property
    def height(self):
        """高度"""
        return self._impl.height

    @property
    def x(self):
        """
        元素在界面上的x轴位置
        """
        return self._impl.x

    @property
    def y(self):
        """
        元素在界面上的y轴位置
        """
        return self._impl.y

    @property
    def top_left(self):
        """
        元素左上角的坐标
        """
        return self._impl.top_left

    @property
    def web_element(self):
        """
        转换为web_element,之后可以调用selenium元素的属性和方法
        """
        return self._impl.web_element

    def __repr__(self):
        if self._is_bound():
            element_html = self.web_element.get_attribute('outerHTML')
            return get_easily_readable_snippet(element_html)
        else:
            return super(HTMLElement, self).__repr__()


class S(HTMLElement):
    """
    元素定位类

    S(".myClass")
    S("#myId")
    S("@btnName")  name="btnName"
     //            对应xpath或者css selector


    Where ``email`` is the column header (``<th>Email</th>``). Similarly to
    ``below`` and ``to_right_of``, the keyword parameters ``above`` and
    ``to_left_of`` can be used to search for elements above and to the left
    of other web elements.
    """
    def __init__(self,
                 selector,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(S, self).__init__(below=below,
                                to_right_of=to_right_of,
                                above=above,
                                to_left_of=to_left_of)
        self._args.append(selector)


class Text(HTMLElement):
    """
    文本控件, 用于获取界面文本的控件，拥有value属性来获取控件对应的文本
    """
    def __init__(self,
                 text=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(Text, self).__init__(below=below,
                                   to_right_of=to_right_of,
                                   above=above,
                                   to_left_of=to_left_of)
        self._args.append(text)

    @property
    def value(self):
        """
        返回文本的值
        """
        return self._impl.value


class Link(HTMLElement):
    """
    用于识别界面上的链接，拥有href属性可以获取链接对应的url
    """
    def __init__(self,
                 text=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(Link, self).__init__(below=below,
                                   to_right_of=to_right_of,
                                   above=above,
                                   to_left_of=to_left_of)
        self._args.append(text)

    @property
    def href(self):
        """
        Returns the URL of the page the link goes to.
        """
        return self._impl.href


class ListItem(HTMLElement):
    """
    可以获取界面上的序列<li>元素
    """
    def __init__(self,
                 text=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(ListItem, self).__init__(below=below,
                                       to_right_of=to_right_of,
                                       above=above,
                                       to_left_of=to_left_of)
        self._args.append(text)


class Button(HTMLElement):
    """
    可以获取按钮控件，具有is_enabled方法，用于判断按钮是否有被disabled
    """
    def __init__(self,
                 text=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(Button, self).__init__(below=below,
                                     to_right_of=to_right_of,
                                     above=above,
                                     to_left_of=to_left_of)
        self._args.append(text)

    def is_enabled(self):
        """
        Returns true if this UI element can currently be interacted with.
        """
        return self._impl.is_enabled()


class Image(HTMLElement):
    """
    识别图片元素,从__init__类的初始化函数可以看到相对其他控件的text=None（，
    图片控件的是alt=None，也就是说要通过alt而非text来识别图片。
    """
    def __init__(self,
                 alt=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(Image, self).__init__(below=below,
                                    to_right_of=to_right_of,
                                    above=above,
                                    to_left_of=to_left_of)
        self._args.append(alt)


class TextField(HTMLElement):
    """
    可识别可以用于输入的文本框控件,它是通过label加相对位置来定位的，
    这个label既可以采用本文框左侧的文字，也可以采用文本框内部的提示语，也就是html的placeholder里的文本。
    """
    def __init__(self,
                 label=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(TextField, self).__init__(below=below,
                                        to_right_of=to_right_of,
                                        above=above,
                                        to_left_of=to_left_of)
        self._args.append(label)

    @property
    def value(self):
        """
        Returns the current value of this text field. '' if there is no value.
        """
        return self._impl.value

    def is_enabled(self):
        """
        识别文本框是否被disabled，界面上显示的就是置灰
        """
        return self._impl.is_enabled()

    def is_editable(self):
        """
        识别文本框是否可编辑，也就是是否有配置readonly，这时控件是不置灰的，但是不可编辑。
        """
        return self._impl.is_editable()


class ComboBox(HTMLElement):
    """
    下拉列表框控件, 一般来说既可以指的select，也可以指dropdown控件

    示例:
    ComboBox("Language").value
    select(ComboBox(to_right_of="John Doe", below="Status"), "Active")
    """
    def __init__(self,
                 label=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(ComboBox, self).__init__(below=below,
                                       to_right_of=to_right_of,
                                       above=above,
                                       to_left_of=to_left_of)
        self._args.append(label)

    def is_editable(self):
        """
        Returns whether this combo box allows entering an arbitrary text in
        addition to selecting predefined values from a drop-down list.
        """
        return self._impl.is_editable()

    @property
    def value(self):
        """
        可以获取已选中项的值
        """
        return self._impl.value

    @property
    def options(self):
        """
        可以获取所有可以选择的选项内容
        """
        return self._impl.options


class CheckBox(HTMLElement):
    """
    识别复选框用的
    """
    def __init__(self,
                 label=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(CheckBox, self).__init__(below=below,
                                       to_right_of=to_right_of,
                                       above=above,
                                       to_left_of=to_left_of)
        self._args.append(label)

    def is_enabled(self):
        """
        判断是否被disabled
        """
        return self._impl.is_enabled()

    def is_checked(self):
        """
        判断是否被选中
        """
        return self._impl.is_checked()


class RadioButton(HTMLElement):
    """
    单选框控件
    """
    def __init__(self,
                 label=None,
                 below=None,
                 to_right_of=None,
                 above=None,
                 to_left_of=None):
        super(RadioButton, self).__init__(below=below,
                                          to_right_of=to_right_of,
                                          above=above,
                                          to_left_of=to_left_of)
        self._args.append(label)

    def is_selected(self):
        """
        判断是否被选中
        """
        return self._impl.is_selected()


class Window(GUIElement):
    """
    可以配合switch_to来进行窗口的切换
    """
    def __init__(self, title=None):
        super(Window, self).__init__()
        self._args.append(title)

    @property
    def title(self):
        """
        Returns the title of this Window.
        """
        return self._impl.title

    @property
    def handle(self):
        """
        Returns the Selenium driver window handle assigned to this window. Note
        that this window handle is simply an abstract identifier and bears no
        relationship to the corresponding operating system handle (HWND on
        Windows).
        """
        return self._impl.handle

    def __repr__(self):
        if self._is_bound():
            return self._repr_constructor_args([self.title])
        else:
            return super(Window, self).__repr__()


class Alert(GUIElement):
    """
    弹窗控件

    可以通过文本来查找弹窗，特别是对于多个弹窗的话，用文本来区分即可，如果只有一个弹窗，可以search_text放空一般也可以找到
    """
    def __init__(self, search_text=None):
        super(Alert, self).__init__()
        self._args.append(search_text)

    @property
    def text(self):
        """
        The text displayed in the alert box.
        """
        return self._impl.text

    def accept(self):
        """
        相当于点击弹窗上的OK / 确认键
        # >>> Alert().accept()
        """
        self._impl.accept()

    def dismiss(self):
        """
        相当于点击弹窗上的Cancel / 取消按键
        # >>> Alert().dismiss()
        """
        self._impl.dismiss()

    def __repr__(self):
        if self._is_bound():
            return self._repr_constructor_args([self.text])
        else:
            return super(Alert, self).__repr__()


class Point(namedtuple('Point', ['x', 'y'])):
    """
    通过x,y来表示元素在界面上的具体坐标，如果要偏移位置，可以用+或者-来实现
    """
    def __new__(cls, x=0, y=0):
        return cls.__bases__[0].__new__(cls, x, y)

    def __init__(self, x=0, y=0):
        # tuple is immutable so we can't do anything here. The initialization
        # happens in __new__(...) above.
        pass

    @property
    def x(self):
        """
        The x coordinate of the point.
        """
        return self[0]

    @property
    def y(self):
        """
        The y coordinate of the point.
        """
        return self[1]

    def __eq__(self, other):  # 对比两个Point看是否相等
        return (self.x, self.y) == other

    def __ne__(self, other):  # 对比两个Point看是否相等
        return not self == other

    def __hash__(self):  # hash，返回x + 7*y，暂时未知作用
        return self.x + 7 * self.y

    def __add__(self, delta):  # 相加，delta对应某个point
        dx, dy = delta
        return Point(self.x + dx, self.y + dy)

    def __radd__(self, delta):
        return self.__add__(delta)

    def __sub__(self, delta):  # 相减
        dx, dy = delta
        return Point(self.x - dx, self.y - dy)

    def __rsub__(self, delta):
        x, y = delta
        return Point(x - self.x, y - self.y)


def _get_api_impl():
    global _API_IMPL
    if _API_IMPL is None:
        _API_IMPL = APIImpl()
    return _API_IMPL


_API_IMPL = None
